#region License

// Another free, open solution by someone who gets it.
// 
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

#endregion

using System;
using System.Collections.Generic;
using System.Diagnostics;

using Patterns.Validation.Framework;

namespace Patterns.Validation.Advanced
{
	/// <summary>
	/// 	Contains a number of methods to assert that two <see cref = "DateTime" /> objects differ in the expected way.
	/// </summary>
	/// <remarks>
	/// 	You can use the <see cref = "FluentDateTimeExtensions" /> and <see cref = "TimeSpanConversionExtensions" /> for a more fluent
	/// 	way of specifying a <see cref = "DateTime" /> or a <see cref = "TimeSpan" />.
	/// </remarks>
	[DebuggerNonUserCode]
	public class TimeSpanValidator
	{
		#region Private Definitions

		private readonly DateTimeValidator _parentValidator;
		private readonly TimeSpanPredicate _predicate;

		private readonly Dictionary<TimeSpanCondition, TimeSpanPredicate> _predicates = new Dictionary
			<TimeSpanCondition, TimeSpanPredicate>
		{
			{ TimeSpanCondition.MoreThan, new TimeSpanPredicate((ts1, ts2) => ts1 > ts2, "more than") },
			{ TimeSpanCondition.LessThan, new TimeSpanPredicate((ts1, ts2) => ts1 < ts2, "less than") },
			{ TimeSpanCondition.AtLeast, new TimeSpanPredicate((ts1, ts2) => ts1 >= ts2, "at least") },
			{ TimeSpanCondition.AtMost, new TimeSpanPredicate((ts1, ts2) => ts1 <= ts2, "at most") },
			{ TimeSpanCondition.Exactly, new TimeSpanPredicate((ts1, ts2) => ts1 == ts2, "exactly") }
		};

		private readonly DateTime? _subject;
		private readonly TimeSpan _timeSpan;

		#endregion

		protected internal TimeSpanValidator(DateTimeValidator parentValidator, DateTime? subject, TimeSpanCondition condition,
		                                     TimeSpan timeSpan)
		{
			_parentValidator = parentValidator;
			_subject = subject;
			_timeSpan = timeSpan;

			_predicate = _predicates[condition];
		}

		/// <summary>
		/// 	Validates that a <see cref = "DateTime" /> occurs a specified amount of time before another <see cref = "DateTime" />.
		/// </summary>
		/// <param name = "target">
		/// 	The <see cref = "DateTime" /> to compare the subject with.
		/// </param>
		public AndConstraint<DateTimeValidator> Before(DateTime target)
		{
			return Before(target, string.Empty);
		}

		/// <summary>
		/// 	Validates that a <see cref = "DateTime" /> occurs a specified amount of time before another <see cref = "DateTime" />.
		/// </summary>
		/// <param name = "target">
		/// 	The <see cref = "DateTime" /> to compare the subject with.
		/// </param>
		/// <param name = "reason">
		/// 	A formatted phrase explaining why the assertion should be satisfied. If the phrase does not 
		/// 	start with the word <i>because</i>, it is prepended to the message.
		/// </param>
		/// <param name = "reasonArgs">
		/// 	Zero or more values to use for filling in any <see cref = "string.Format(string,object[])" /> compatible placeholders.
		/// </param>
		public AndConstraint<DateTimeValidator> Before(DateTime target, string reason, params object[] reasonArgs)
		{
			TimeSpan actual = target.Subtract(_subject.Value);

			if (!_predicate.IsMatchedBy(actual, _timeSpan))
			{
				Execute.Verification
					.BecauseOf(reason, reasonArgs)
					.FailWith(
						"Expected date and/or time {0} to be " + _predicate.DisplayText +
						" {1} before {2}{reason}, but it differs {3}.",
						_subject, _timeSpan, target, actual);
			}

			return new AndConstraint<DateTimeValidator>(_parentValidator);
		}

		/// <summary>
		/// 	Validates that a <see cref = "DateTime" /> occurs a specified amount of time after another <see cref = "DateTime" />.
		/// </summary>
		/// <param name = "target">
		/// 	The <see cref = "DateTime" /> to compare the subject with.
		/// </param>
		public AndConstraint<DateTimeValidator> After(DateTime target)
		{
			return After(target, string.Empty);
		}

		/// <summary>
		/// 	Validates that a <see cref = "DateTime" /> occurs a specified amount of time after another <see cref = "DateTime" />.
		/// </summary>
		/// <param name = "target">
		/// 	The <see cref = "DateTime" /> to compare the subject with.
		/// </param>
		/// <param name = "reason">
		/// 	A formatted phrase explaining why the assertion should be satisfied. If the phrase does not 
		/// 	start with the word <i>because</i>, it is prepended to the message.
		/// </param>
		/// <param name = "reasonArgs">
		/// 	Zero or more values to use for filling in any <see cref = "string.Format(string,object[])" /> compatible placeholders.
		/// </param>
		public AndConstraint<DateTimeValidator> After(DateTime target, string reason, params object[] reasonArgs)
		{
			TimeSpan actual = _subject.Value.Subtract(target);

			if (!_predicate.IsMatchedBy(actual, _timeSpan))
			{
				Execute.Verification
					.BecauseOf(reason, reasonArgs)
					.FailWith(
						"Expected date and/or time {0} to be " + _predicate.DisplayText +
						" {1} after {2}{reason}, but it differs {3}.",
						_subject, _timeSpan, target, actual);
			}

			return new AndConstraint<DateTimeValidator>(_parentValidator);
		}

		#region Nested type: TimeSpanPredicate

		/// <summary>
		/// 	Provides the logic and the display text for a <see cref = "TimeSpanCondition" />.
		/// </summary>
		private class TimeSpanPredicate
		{
			private readonly string _displayText;
			private readonly Func<TimeSpan, TimeSpan, bool> _lambda;

			public TimeSpanPredicate(Func<TimeSpan, TimeSpan, bool> lambda, string displayText)
			{
				_lambda = lambda;
				_displayText = displayText;
			}

			public string DisplayText
			{
				get { return _displayText; }
			}

			public bool IsMatchedBy(TimeSpan actual, TimeSpan expected)
			{
				return _lambda(actual, expected);
			}
		}

		#endregion
	}
}